﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Xml;
using XmlServer.ServiceContracts;
using XmlServer.ServiceImplementation;

namespace XmlServer.Helper
{
    public class Utilities
    {
        #region Miscellaneous Helpers

        public static string GetLocalIpAddress()
        {
            var host = Dns.GetHostEntry(Dns.GetHostName());

            foreach (var ip in host.AddressList)
            {
                if (ip.AddressFamily == AddressFamily.InterNetwork)
                {
                    return ip.ToString();
                }

            }

            return String.Empty;

        }
       
        #endregion

        #region Xml Helers

        public static XmlDocument GetXmlDocument(string RequestXML)
        {
            XmlDocument doc = new XmlDocument();
            doc.LoadXml(RequestXML);
            return doc;
        }

        public static XmlNode GetNode(XmlDocument xmlDoc, string NodeName)
        {
            return xmlDoc.SelectSingleNode(NodeName);
        }

        public static XmlNode GetRequestNode(XmlDocument xmlDoc)
        {
            return GetNode(xmlDoc, "Request");
        }

        public static XmlNode GetResponsetNode(XmlDocument xmlDoc)
        {
            return GetNode(xmlDoc, "Response");
        }

        public static Dictionary<string, string> GetRequestAttributes(string RequestXML)
        {
            var doc = GetXmlDocument(RequestXML);
            return GetRequestAttributes(doc);
        }

        public static Dictionary<string, string> GetRequestAttributes(XmlDocument doc)
        {
            var requestNode = GetRequestNode(doc);
            return GetRequestAttributes(requestNode);
        }

        public static Dictionary<string, string> GetRequestAttributes(XmlNode requestNode)
        {
            Dictionary<string, string> results = new Dictionary<string, string>();

            foreach (XmlAttribute attribute in requestNode.Attributes)
            {
                if (!results.ContainsKey(attribute.Name))
                {
                    results.Add(attribute.Name, attribute.Value);
                }
            }

            return results;
        }

        public static void RemoveRelayAttributes(XmlDocument requestXml)
        {
            var requestNode = GetRequestNode(requestXml);
            requestNode.Attributes.RemoveNamedItem("RelayIPAddress");
            requestNode.Attributes.RemoveNamedItem("RelayPort");
        }

        public static bool IsResponseSuccess(string ResponseXML)
        {
            bool success = false; // assume a failure

            var doc = GetXmlDocument(ResponseXML);
            var response = GetResponsetNode(doc);

            if (response.Attributes != null)
            {
                XmlAttribute successAttribute = null;
                try
                {
                    successAttribute = response.Attributes["Success"];
                }
                catch
                {
                    successAttribute = null;
                }

                if (successAttribute != null)
                {
                    success = successAttribute.Value.Equals("true", StringComparison.InvariantCultureIgnoreCase);
                }

            }

            return success;

        }

        public static string CleanNodeName(string Name)
        {
            var clean = Regex.Replace(Name, @"\W|_", string.Empty);
            return clean.Replace(" ", "_");
        }
        
        #endregion

        #region Facts Helpers


        public static List<IFact> GetFacts(string RequestXml, bool IsResponse)
        {
            var doc = GetXmlDocument(RequestXml);
            return GetFacts(doc, IsResponse);
        }


        public static List<IFact> GetFacts(XmlDocument doc, bool IsResponse)
        {
            List<IFact> facts = new List<IFact>();

            var rootNode = GetNode(doc, IsResponse ? "Response" : "Request");
            XmlNodeList childNodes = rootNode.ChildNodes;

            foreach (XmlNode node in childNodes)
            {
                facts.Add(new Fact
                    {
                        Name = node.Name,
                        Value = node.InnerText
                    });
            }

            return facts;
        }

        #endregion

        #region Client Helpers

        /// <summary>
        /// Helps a client application create a properly formatted Xml Request for a Operator
        /// </summary>
        public static string CreateRequest(string OperatorName, string OperatorVersion, List<IFact> Facts)
        {
            StringBuilder sb = new StringBuilder();
            XmlWriter xml = XmlWriter.Create(sb);

            xml.WriteStartElement("Request");
            xml.WriteAttributeString("Type", OperatorName);
            xml.WriteAttributeString("Version", OperatorVersion);

            if (Facts != null && Facts.Any())
            {
                foreach (var fact in Facts)
                {
                    xml.WriteElementString(fact.Name, fact.Value);
                }
                xml.WriteEndElement();
            }


            xml.WriteEndDocument();
            xml.Flush();
            xml.Close();

            return sb.ToString();

        }


        /// <summary>
        /// Helps a client application create a properly formatted Xml Request for a Operator, with a Relay IpAddress and Port
        /// </summary>
        public static string CreateRequest(string OperatorName, string OperatorVersion, List<IFact> Facts, string relayIPAddress, int relayPort)
        {
            StringBuilder sb = new StringBuilder();
            XmlWriter xml = XmlWriter.Create(sb);

            xml.WriteStartElement("Request");
            xml.WriteAttributeString("Type", OperatorName);
            xml.WriteAttributeString("Version", OperatorVersion);
            xml.WriteAttributeString("RelayIPAddress", relayIPAddress);
            xml.WriteAttributeString("RelayPort", relayPort.ToString());

            if (Facts != null && Facts.Any())
            {
                foreach (var fact in Facts)
                {
                    xml.WriteElementString(fact.Name, fact.Value);
                }
                xml.WriteEndElement();
            }


            xml.WriteEndDocument();
            xml.Flush();
            xml.Close();

            return sb.ToString();
        }


        /// <summary>
        /// Response string to be returned to the client
        /// </summary>        
        public static string CreateResponse(string OperatorName, string Version, bool Success, List<IFact> facts)
        {
            StringBuilder sb = new StringBuilder();
            XmlWriter xml = XmlWriter.Create(sb);

            xml.WriteStartElement("Response");
            xml.WriteAttributeString("Type", OperatorName);
            xml.WriteAttributeString("Version", Version);
            xml.WriteAttributeString("Success", Success.ToString().ToLower());

            if (facts != null && facts.Any())
            {
                foreach (var fact in facts)
                {
                    try
                    {
                        xml.WriteElementString(
                            CleanNodeName(fact.Name),
                            XmlTextEncoder.Encode(fact.Value));
                    }
                    catch (Exception err)
                    {
                        // TODO: failed to add data to the response XML....need to do somthine here!
                    }
                }
                xml.WriteEndElement();
            }


            xml.WriteEndDocument();
            xml.Flush();
            xml.Close();

            return sb.ToString();

        }
        


        public static string CreateResponse(IResponse response)
        {
            StringBuilder sb = new StringBuilder();
            XmlWriter xml = XmlWriter.Create(sb);

            xml.WriteStartElement("Response");
            xml.WriteAttributeString("Type", response.Request.Name);
            xml.WriteAttributeString("Version", response.Request.Version);
            xml.WriteAttributeString("Success", response.Success.ToString());

            if (response.Facts != null && response.Facts.Any())
            {
                foreach (var fact in response.Facts)
                {
                    try
                    {
                        xml.WriteElementString(
                            CleanNodeName(fact.Name),
                            XmlTextEncoder.Encode(fact.Value));
                    }
                    catch (Exception err)
                    {
                        // TODO: failed to add data to the response XML....need to do somthine here!
                    }
                }
                xml.WriteEndElement();
            }


            xml.WriteEndDocument();
            xml.Flush();
            xml.Close();

            return sb.ToString();

        }

        /// <summary>
        /// Exception string to be returned to the client
        /// </summary>
        public static string CreateException(string OperatorName, string Version, string ErrorMsg)
        {
            return CreateException(OperatorName, Version, new Exception(ErrorMsg));
        }

        /// <summary>
        /// Exception string to be returned to the client
        /// </summary>
        public static string CreateException(string OperatorName, string Version, Exception err)
        {

            List<IFact> facts = new List<IFact>();
            PopulateException(facts, err, 1);
            return CreateResponse(OperatorName, Version, false, facts);
        }

        public static List<IFact> CreateException(Exception err)
        {
            List<IFact> facts = new List<IFact>();
            PopulateException(facts, err, 1);
            return facts;
        }

        public static List<IFact> CreateException(string ErrorMsg)
        {
            return CreateException(new Exception(ErrorMsg));
        }

        private static void PopulateException(List<IFact> facts, Exception err, int Level)
        {
            facts.Add(new Fact { Name = String.Format("ErrorMessage{0}", Level), Value = err.Message });
            facts.Add(new Fact { Name = String.Format("ErrorSource{0}", Level), Value = err.Source });
            facts.Add(new Fact { Name = String.Format("StackTrace{0}", Level), Value = err.StackTrace });

            try
            {
                if (err.Data != null && err.Data.Keys != null)
                {
                    var keys = err.Data.Keys;
                    foreach (var key in keys)
                    {
                        try
                        {
                            facts.Add(new Fact { Name = String.Format("Error{0}Data{1}", Level, key), Value = err.Data[key].ToString() });
                        }
                        catch { }
                    }
                }
            }
            catch { }

            if (err.InnerException != null)
            {
                int nextLevel = Level + 1;
                PopulateException(facts, err.InnerException, nextLevel);
            }
        }

        /// <summary>
        /// Used by client applications to send the request string to the specifed xml server
        /// </summary>
        public static string MakeRequest(string request, string ipAddress, int port)
        {
            string response = "";

            TcpClient client = null;
            try
            {
                // create the tcp client
                try
                {
                    client = new TcpClient();
                    client.ReceiveTimeout = 10000;
                    client.SendTimeout = 10000;

                    //client.Connect(ipAddress, port);

                    
                    bool isConnected = false;
                    for (int i = 0; i <= 5; i++)
                    {
                        try
                        {
                            client.Connect(ipAddress, port);
                            isConnected = true;
                            break;
                        }
                        catch
                        {
                            Thread.Sleep(5000);
                        }

                        if (!isConnected)
                        {
                            throw new Exception("Failed to connect to the XmlServer");
                        }
                    }
                    
                    
                }
                catch (Exception err)
                {
                    throw new Exception(String.Format("Failed to create the connection to the xml server {0}:{1}", ipAddress, port), err);
                }

                // create the network stream for read/write
                NetworkStream stream = null;
                try
                {
                    stream = client.GetStream();
                }
                catch (Exception err)
                {
                    throw new Exception(String.Format("Failed to create the network stream to the xml server {0}:{1}", ipAddress, port), err);
                }


                if (!stream.CanWrite)
                {
                    throw new Exception(String.Format("Network stream is not writable at {0}:{1}", ipAddress, port));
                }
                if (!stream.CanRead)
                {
                    throw new Exception(String.Format("Network stream is not readable at {0}:{1}", ipAddress, port));
                }


                // send the request
                try
                {


                    byte[] sendBytes = Encoding.ASCII.GetBytes(request);

                    var sendBytesLength = sendBytes.Length;
                    var tcpSendBufferSize = client.SendBufferSize;
                    if (sendBytesLength <= tcpSendBufferSize)
                    {
                        // the request is small enough to send
                        stream.Write(sendBytes, 0, sendBytes.Length);
                    }
                    else
                    {
                        // the request is too large; it needs to be sent in "chunks"
                        int offset = 0;
                        var chunkSize = tcpSendBufferSize;

                        while (offset < sendBytesLength)
                        {
                            stream.Write(sendBytes, offset, chunkSize);

                            offset += chunkSize;
                            if (offset == sendBytesLength) break;
                            if (offset + chunkSize > sendBytesLength) chunkSize = sendBytesLength - offset;

                        }

                    }


                    // old code - does not work for large requests
                    /*
                    byte[] sendBytes = Encoding.ASCII.GetBytes(request);
                    stream.Write(sendBytes, 0, sendBytes.Length);
                    */

                }
                catch (Exception err)
                {
                    throw new Exception(String.Format("Failed to write request to network stream/tcp client at {0}:{1}", ipAddress, port), err);
                }


                // read the response
                try
                {

                    StringBuilder sbResponse = new StringBuilder();
                    try
                    {
                        byte[] bytes = new byte[client.ReceiveBufferSize];
                        int bytesRead = stream.Read(bytes, 0, client.ReceiveBufferSize);
                        while (bytesRead > 0)
                        {
                            sbResponse.Append(Encoding.ASCII.GetString(bytes, 0, bytesRead));
                            bytesRead = stream.Read(bytes, 0, client.ReceiveBufferSize);
                        }
                    }
                    catch (IOException)
                    {
                        // all of the data has been recevied   
                    }

                    response = sbResponse.ToString();


                }
                catch (Exception err)
                {
                    throw new Exception(String.Format("Failed to write request to network stream/tcp client at {0}:{1}", ipAddress, port), err);
                }

            }
            catch (Exception err)
            {
                throw;
            }
            finally
            {
                if (client != null && client.Connected) client.Close();
            }


            return response;

        }


        /// <summary>
        /// Get a value from the facts collection and cast to the data type
        /// Note: this throws an exception if key not found or cast fails
        /// </summary>
        public static T GetFact<T>(List<IFact> facts, string Name)
        {
            var fact = facts.FirstOrDefault(p => p.Name.Equals(Name, StringComparison.InvariantCultureIgnoreCase));
            if (fact == null) throw new ArgumentException(String.Format("Fact '{0}' not found", Name));


            T castedValue;
            try
            {
                castedValue = (T)Convert.ChangeType(fact.Value, typeof(T));
            }
            catch (Exception err)
            {
                throw new Exception("Failed to cast value of '{0}'", err);
            }

            return castedValue;

        }

        public static string GetFact(List<IFact> facts, string Name)
        {
            return GetFact<string>(facts, Name);
        }


        /// <summary>
        /// Get a value from the facs collection and cast to the data type
        /// Note: this returns the default value if key not found or cast exception
        /// </summary>
        public static T GetFact<T>(List<IFact> facts, string Name, T defaultValue)
        {

            var fact = facts.FirstOrDefault(p => p.Name.Equals(Name, StringComparison.InvariantCultureIgnoreCase));
            if (fact == null) return defaultValue;


            T castedValue;
            try
            {
                castedValue = (T)Convert.ChangeType(fact.Value, typeof(T));
            }
            catch
            {
                castedValue = defaultValue;
            }

            return castedValue;

        }

        public static string GetFact(List<IFact> facts, string Name, string defaultValue)
        {
            return GetFact<string>(facts, Name, defaultValue);
        }

        public static bool ContainsFact(List<IFact> facts, string Name)
        {
            var fact = facts.FirstOrDefault(p => p.Name.Equals(Name, StringComparison.InvariantCultureIgnoreCase));
            return (fact != null);
        }

        #endregion



    }
}
